R8, Proguard
1. Proguard, R8?
=> 바이트코드 최적화와 난독화를 위해 사용되는 도구이다.
1-1. 난독화를 꼭 해야할까?
안드로이드 애플리케이션 코드를 난독화하려는 이유는 리버싱이 가능하기 때문이다. apk 파일을 가지고 개발자가 작성한 자바 코드를 재현해내는 것이 가능하다는 것이다. 이것은 바이트 코드의 특징과 apk 파일의 구조 때문이다. 자바 소스코드를 컴파일해서 생성된 바이트코드(.class)파일은 원본의 자바 소스코드와 형태가 유사하고 기계어가 아니기 때문에 인간이 해석할 수 있을 정도의 수준이므로 역변환 하기 쉽다. apk 파일 또한 dex 파일과 리소스파일, 매니페스트등을 모아서 압축해놓은 패키지이므로 이것에서 dex를 추출하는것도 가능하다(안드로이드 APK 구조 참고-Android APK Build Analysis, CAPTAINWONJONG). 이런 구조적인 특징 때문에 안드로이드의 소스코드를 난독화 하는것은 필요하다. (iOS는 네이티브 ARM 바이너리 코드로 컴파일된다고 한다.)
1-2. 어떤 최적화를 해줄까?
프로가드나 R8을 사용하는 주된 이유는 난독화라고 생각한다. 하지만 이 도구를 잘 사용하면 코드의 최적화 또한 달성할 수 있다. 프로가드의 최적화는 간단하게 사용하지 않는 코드제거 정도의 최적화를 해준다고 한다. 반면에 R8은 코드 제거뿐 아니라 재작성(클래스와 메서드의 인라인화, 제어흐름 최적화 등)(참고-Android Doc)도 해준다.
1-3. 프로가드 과정은 언제 진행될까?
소스코드를 컴파일한 바이트 코드가 생성되면, 프로가드 도구는 그 바이트 코드를 처리한다. 즉, javac의 작업 후, DX(D8)의 작업 전 프로가드가 실행된다. ProGurad는 최적화 단계와 난독화 단계가 별도로 진행되며 R8은 동시에 처리된다.
1-3. R8은 Proguard에 비해 어떤 좋은점이 있을까?
1-2에서 최적화는 설명했다. 조금 더 덧붙이자면 R8은 D8과 같이 작업하기 때문에(R8은 안드로이드 빌드 시스템의 일부이다.) D8에서 발생하는 정보를 이용해 추가적인 최적화를 할 수 있게된다. 그리고 R8은 1-3.의 설명에서처럼 난독화와 최적화를 동시에 처리하기 때문에 향상된 빌드 속도를 보인다.
+R8이 AGP 3.3.0에서 도입되었고 3.4.0에서 기본으로 설정되었는데 현재 최신 AGP 버전이 8.6.0인것을 생각하면 Proguard라는 용어가 이 도구의 역할을 명확하게 나타내주는 단어이기 때문이 아닐까 생각한다. 현재 실제로는 Proguard가 아닌 R8이 사용되고 있다는 것만 기억하자.
2. 적용 방법
2-1. Gradle 설정
Android Doc - 축소, 난독화 및 최적화 사용
android {
buildTypes {
getByName("release") {
//코드 축소, 난독화, 최적화를 하고싶다면 true (디버그 모드로 빌드할 수 없음. isDebuggable=false)
isMinifyEnabled = true
//필요 없는 리소스 삭제
isShrinkResources = true
proguardFiles(
// Android Gradle 플러그인에 있는 기본 Rule
getDefaultProguardFile("proguard-android-optimize.txt"),
// 개발자가 정의한 Rule
"proguard-rules.pro"
)
}
}
...
}
2-2. 프로가드 규칙
Android Doc - 유지할 코드 맞춤 설정
안드로이드 앱 난독화 알아보기, five2week
패키지, 클래스명, 변수, 애노테이션에 대한 적용
// 특정 가시성 클래스에 적용하지 않기
-keep public class * { *; }
// 특정 클래스에 적용하지 않기
-keep com.example.mypackage.MyClass { *; }
// 특정 패키지에 적용하지 않기
-keep com.example.mypackage.* { *; }
// 특정 패키지와 하위 패키지에 있는 클래스들에 적용하지 않기
-keep com.example.mypackage.** { *; }
// 클래스 이름 난독화 하지 않기
-keepclassnames com.example.mypackage.MyClass
// 클래스 내의 모든 클래스 맴버 난독화하지 않기
-keepclassmembers com.example.mypackage.MyClass {
*;
}
// 클래스 내의 private 클래스 맴버에 대해서 난독화하지 않기
-keepclassmembers com.example.mypackage.MyClass {
private *;
}
// Annotation으로 끝나는 애노테이션 클래스에 대해 난독화 적용하지 않기
-keep @interface com.example.mypackage.*Annotation
3. 왜 난독화를 적용할 수 없는 부분이 생길까?
Android Doc-유지할 코드 맞춤설정
난독화는 클래스나 변수의 이름을 알아볼 수 없게 바꾸는 것이다. 이런 난독화를 하지 말아야 한다면 클래스나 변수의 이름을 사용해야 하는 경우일 것이다. 그러한 경우는 두가지가 있다.
- JNI(Java Native Interface)의 메서드를 호출하는 경우
- 리플랙션을 사용하는 경우
3-1. JNI의 메서드를 호출하는 경우
GPT가 생성한 JNI 호출 코드를 보자
여기에서 getGreetingMessage() 메서드의 이름이 난독화된다면 native-lib.cpp 네이티브코드의 getGreetingMessage() 메서드를 찾아서 호출할 수 없게된다.
public class MainActivity extends AppCompatActivity {
// Native method 선언
public native String getGreetingMessage();
static {
// JNI 라이브러리 로드
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// native 메소드 호출
String message = getGreetingMessage();
}
}
3-2. 리플랙션을 사용하는경우
3-1.의 경우와 비슷하게 리플랙션도 메소드의 이름을 사용하여 런타임시에 해당 메소드를 호출한다. 아래의 예시는 ExampleClass 클래스에 정의된 sayHello() 메서드를 호출하는 리플랙션이다. ExampleClass와 sayHello()가 난독화되었다면 이 리플랙션은 정상적인 동작을 할 수 없게된다.
import java.lang.reflect.Method;
public class MainActivity {
public static void main(String[] args) {
try {
// 클래스 객체를 얻기 위한 리플렉션
Class<?> clazz = Class.forName("ExampleClass");
// 메소드 정보를 얻기 위한 리플렉션
Method method = clazz.getDeclaredMethod("sayHello", String.class);
// 메소드 호출
Object instance = clazz.getDeclaredConstructor().newInstance();
method.invoke(instance, "World");
} catch (Exception e) {
e.printStackTrace();
}
}
}
리플랙션으로 인해서 난독화를 주의해서 사용해야 하는 라이브러리나 기능들에는,
- GSON
- Retrofit
- Serializable
- Annotation Processing
등이 있다.
3-3. 이외에도...
- 외부 라이브러리, SDK를 사용하는 경우 (3-1.와 비슷한 이유로)
- Kotlin의 data class에서 코틀린이 자동으로 생성해주는 메서드 (copy, componentN)등을 사용하는 경우
에도 난독화를 주의해서 사용해야 한다.
4. 기타 참고
- ByteCode 설명, JVM, JIT, Android 빌드 과정, ART, R8에 대한 설명
Android CPU, Compilers, D8 & R8